#!/usr/bin/python3
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t -*-

import os
import time
import glob
import shutil
import mirrors.plugin
from dataclasses import dataclass
from libtorrent import session as torrent_session
from libtorrent import torrent_info
from libtorrent import torrent_handle


class Main:

    # from https://github.com/cncases/cases

    def __init__(self):
        self.cfg = mirrors.plugin.params["config"]
        self.dataDir = mirrors.plugin.params["storage-file"]["data-directory"]

        self.downloadingDir = os.path.join(self.dataDir, ".downloading")
        self.seedingDir = os.path.join(self.dataDir, ".seeding")

    def run(self):
        selfDir = os.path.dirname(os.path.realpath(__file__))
        torrentFullfnList = glob.glob(os.path.join(selfDir, "*.torrent"))

        os.makedirs(self.downloadingDir, exist_ok=True)
        os.makedirs(self.seedingDir, exist_ok=True)

        self._removeRedundantFiles(torrentFullfnList)

        # download torrent files
        if True:
            record = []

            downloader = TorrentDownloader()
            try:
                # prepare tmpDir for each torrent file in selfDir, add torrent file into downloader
                for torrentFullfn in torrentFullfnList:
                    torrentFn = os.path.basename(torrentFullfn)
                    seedingParam = self._getSeedingParamsByTorrentFullfn(torrentFullfn)
                    if os.path.exists(seedingParam.resultDirFullfn):
                        # download completed
                        continue
                    downloadParam = self._getDownloadingParamsByTorrentFullfn(torrentFullfn)
                    downloader.addTorrent(torrentFn, torrentFullfn, None, downloadParam.resultDirFullfn)
                    record.append(torrentFn)

                # download, process progress and completed files
                print("Start downloading %d torrents..." % (len(record)))
                downloader.startDownload()
                while True:
                    ret = downloader.pollCompleteTorrent()
                    if ret is None:
                        break

                    if ret[0]:
                        # some torrents are complete
                        for torrentFn in ret[1]:
                            torrentFullfn = os.path.join(selfDir, torrentFn)
                            downloadingParam = self._getDownloadingParamsByTorrentFullfn(torrentFullfn)
                            seedingParam = self._getSeedingParamsByTorrentFullfn(torrentFullfn)
                            Util.forceMove(downloadingParam.resultDirFullfn, seedingParam.resultDirFullfn)

                        print("Download Complete:")
                        for name in ret[1]:
                            print("    %s" % (name))
                    else:
                        # no torrent is complete
                        print("Progress:")
                        for torrentFn, s in ret[1].items():
                            print('    %s (%.2f%%) %.2f MB/s peers: %d' % (torrentFn, s.progress * 100, s.download_rate / 1000000, s.num_peers))

                        # wait for 5 minute before checking status next time, won't affect torrent downloading becaure it is in other threads
                        time.sleep(5 * 60)
            finally:
                downloader.dispose()

            # create symlinks
            for torrentFullfn in torrentFullfnList:
                seedingParam = self._getSeedingParamsByTorrentFullfn(torrentFullfn)
                fullfn = os.path.join(self.dataDir, os.path.basename(seedingParam.symlinkTarget))
                if not os.path.exists(os.path.join(self.dataDir, seedingParam.symlinkTarget)):
                    raise Exception("%s does not exist" % (os.path.join(self.dataDir, seedingParam.symlinkTarget)))
                os.symlink(seedingParam.symlinkTarget, fullfn)

        print("All done.")

    def _removeRedundantFiles(self, torrentFullfnList):
        # process self.seedingDir and self.dataDir
        if True:
            validFiles = []
            for torrentFullfn in torrentFullfnList:
                seedingParam = self._getSeedingParamsByTorrentFullfn(torrentFullfn)
                if os.path.isdir(seedingParam.resultDirFullfn):
                    validFiles.append(seedingParam.resultDirFullfn)
                fullfn = os.path.join(self.dataDir, os.path.basename(seedingParam.symlinkTarget))
                if os.path.islink(fullfn) and os.readlink(fullfn) == seedingParam.symlinkTarget:
                    validFiles.append(fullfn)

            # remove redundant files
            for fullfn in glob.glob(os.path.join(self.dataDir, "*")):
                if fullfn != self.seedingDir and fullfn != self.downloadingDir and fullfn not in validFiles:
                    Util.forceDelete(fullfn)
            for fullfn in glob.glob(os.path.join(self.seedingDir, "*")):
                if fullfn not in validFiles:
                    Util.forceDelete(fullfn)

        # process self.downloadingDir
        if True:
            validFiles = []
            for torrentFullfn in torrentFullfnList:
                seedingParam = self._getSeedingParamsByTorrentFullfn(torrentFullfn)
                downloadingParam = self._getDownloadingParamsByTorrentFullfn(torrentFullfn)
                if not os.path.isdir(seedingParam.resultDirFullfn) and os.path.isdir(downloadingParam.resultDirFullfn):
                    validFiles.append(downloadingParam.resultDirFullfn)

            # remove redundant files
            for fullfn in glob.glob(os.path.join(self.downloadingDir, "*")):
                if fullfn not in validFiles:
                    Util.forceDelete(fullfn)

    def _getDownloadingParamsByTorrentFullfn(self, torrentFullfn):
        torrentFn = os.path.basename(torrentFullfn)
        return DownloadingParams(
            os.path.join(self.downloadingDir, torrentFn.replace(".torrent", "")),               # download dir full path
        )

    def _getSeedingParamsByTorrentFullfn(self, torrentFullfn):
        torrentFn = os.path.basename(torrentFullfn)
        name = torrentFn.replace(".torrent", "")
        ti = torrent_info(torrentFullfn)
        if torrent_info.name(ti).endswith(".7z"):
            # FIXME: single file torrent have no inner directory
            symlinkTarget = os.path.join(os.path.basename(self.seedingDir), name)
        else:
            symlinkTarget = os.path.join(os.path.basename(self.seedingDir), name, torrent_info.name(ti))
        return SeedingParams(
            os.path.join(self.seedingDir, name),      # full path of directory for bittorrent
            symlinkTarget,                            # target of symlink for httpdir/ftp
        )


@dataclass
class DownloadingParams:
    resultDirFullfn: str


@dataclass
class SeedingParams:
    resultDirFullfn: str
    symlinkTarget: str


class TorrentDownloader:

    def __init__(self):
        self._torrentDict = {}
        self._session = None

    def addTorrent(self, name, torrentFilePath, torrentCfgFilePath, tmpDir):
        assert self._session is None
        ti = torrent_info(torrentFilePath)
        self._torrentDict[torrent_info.name(ti)] = (torrentFilePath, torrentCfgFilePath, tmpDir, ti, name)

    def startDownload(self):
        assert self._session is None

        self._session = torrent_session()
        try:
            # change settings
            settings = self._session.get_settings()
            settings["active_downloads"] = -1
            settings["active_seeds"] = -1
            self._session.apply_settings(settings)

            # add torrents
            for torrentFilePath, torrentCfgFilePath, tmpDir, ti, name in self._torrentDict.values():
                self._session.add_torrent({
                    'ti': ti,
                    'save_path': tmpDir,
                })
        except BaseException:
            self._session = None        # FIXME: destroy session?
            raise

    def pollCompleteTorrent(self):
        assert self._session is not None

        if len(self._session.get_torrents()) == 0:
            return None

        completeList = []
        statusDict = {}

        for handle in self._session.get_torrents():
            torrentFilePath, torrentCfgFilePath, tmpDir, ti, name = self._torrentDict[handle.name()]
            s = handle.status()
            if not s.is_finished:
                statusDict[name] = s
            else:
                Util.deleteRedundantFilesFromTorrentResultDir(tmpDir, handle=handle)
                self._session.remove_torrent(handle)
                completeList.append(name)

        if len(completeList) > 0:
            return (True, completeList)
        else:
            return (False, statusDict)

    def dispose(self):
        self._session = None            # FIXME: destroy session?
        self._torrentDict = {}


class Util:

    @staticmethod
    def forceDelete(path):
        if os.path.islink(path):
            os.remove(path)
        elif os.path.isfile(path):
            os.remove(path)
        elif os.path.isdir(path):
            shutil.rmtree(path)
        elif os.path.lexists(path):
            os.remove(path)             # other type of file, such as device node
        else:
            pass                        # path does not exist, do nothing

    @staticmethod
    def forceMove(srcPath, dstPath):
        Util.forceDelete(dstPath)                # FIXME: needed?
        shutil.move(srcPath, dstPath)

    @staticmethod
    def deleteRedundantFilesFromTorrentResultDir(resultDir, handle=None, torrent_path=None, torrent_cfg_path=None):
        if handle is not None:
            assert torrent_path is None and torrent_cfg_path is None
            ti = torrent_handle.torrent_file(handle)
        else:
            assert torrent_path is not None
            ti = torrent_info(torrent_path)

        # remove files that are not in torrent
        pathList = [x.path for x in torrent_info.files(ti)]
        for dirpath, dirnames, filenames in os.walk(resultDir):
            for filename in filenames:
                fullfn = os.path.join(dirpath, filename)
                if Util.removePathPrefix(fullfn, resultDir) not in pathList:
                    os.remove(fullfn)

        # remove empty directores
        for dirpath, dirnames, filenames in os.walk(resultDir):
            for dirname in dirnames:
                fullfn = os.path.join(dirpath, dirname)
                if len(os.listdir(fullfn)) == 0:
                    os.rmdir(fullfn)

    @staticmethod
    def removePathPrefix(fullfn, prefix):
        assert fullfn[:len(prefix + "/")] == prefix + "/"
        return fullfn[len(prefix + "/"):]


###############################################################################

if __name__ == "__main__":
    Main().run()
